#include <stx/test/test.hpp>

TEST_CASE (core_encoding)
{
    LOOP_N (100)
    {
        auto       buf      = genRandBuffer(100, 10000);
        auto const enc_size = getBase64EncodedSize(buf.size());
        auto const b64_str  = base64Encode(buf);
        CHECK(b64_str.size() == enc_size);
        CHECK(isBase64String(b64_str));

        auto const dec_size = getBase64DecodedSize(b64_str);
        auto const dec_buf  = base64Decode(b64_str);
        CHECK(buf == dec_buf);
        CHECK(dec_size == dec_buf.size());
    }

    LOOP_N (100)
    {
        auto       buf      = genRandBuffer(100, 10000);
        auto const enc_size = getBase64EncodedSize(buf.size());
        auto const b64_str  = base64Encode(buf);

        CHECK(b64_str.size() == enc_size);
        CHECK(isBase64String(b64_str));

        auto shr      = shell("base64"_sv, buf).get();
        auto b64_str2 = mcat2<std::string>(shr.cout);

        CHECK(b64_str == b64_str2);
        auto const dec_size = getBase64DecodedSize(b64_str);

        auto srr     = shellRaw("base64 --decode"_sv, b64_str).get();
        auto dec_buf = mcat2<Buffer>(srr.cout);

        CHECK(dec_size == dec_buf.size());
        CHECK(buf == dec_buf);

        dec_buf = base64Decode(b64_str);
        CHECK(buf == dec_buf);
        CHECK(dec_size == dec_buf.size());
    }

    LOOP_N (100)
    {
        auto       buf      = genRandBuffer(100, 10000);
        auto const enc_size = getBase64EncodedSize(buf.size());
        auto const b64_str  = base64Encode(buf);
        CHECK(b64_str.size() == enc_size);
        CHECK(isBase64String(b64_str));

        auto       srr      = shellRaw("base64 --decode"_sv, b64_str).get();
        auto       dec_buf  = mcat2<Buffer>(srr.cout);
        auto const dec_size = getBase64DecodedSize(b64_str);

        CHECK(dec_size == dec_buf.size());
        CHECK(buf == dec_buf);
    }

    LOOP_N (100)
    {
        auto       buf      = genRandBuffer(100, 10000);
        auto const enc_size = getBase64EncodedSize(buf.size());
        auto       shr      = shell("base64"_sv, buf).get();
        auto       b64_str  = mcat2<std::string>(shr.cout);

        CHECK(b64_str.size() == enc_size, b64_str.size(), enc_size);
        CHECK(isBase64String(b64_str));

        auto const dec_size = getBase64DecodedSize(b64_str);
        auto const dec_buf  = base64Decode(b64_str);

        CHECK(buf == dec_buf);
        CHECK(dec_size == dec_buf.size());
    }

    LOOP_N (100)
    {
        auto str = std::string{};
        str.resize(rand<std::size_t>(10, 100));
        randomize(str);

        auto const enc_str = percentEncode(str);

        for (auto i = 0uz; i < enc_str.size();)
        {
            auto const c = enc_str[i];
            if (isAlphanumeric(c))
            {
                ++i;
                continue;
            }

            if ('-' == c || '_' == c || '.' == c || '~' == c)
            {
                ++i;
                continue;
            }

            CHECK('%' == c);
            CHECK(i + 2 < enc_str.size(), i, enc_str.size());

            auto const hi = enc_str[i + 1];
            auto const lo = enc_str[i + 2];

            CHECK((hi >= '0' && hi <= '9') || (hi >= 'A' && hi <= 'F'));
            CHECK((lo >= '0' && lo <= '9') || (lo >= 'A' && lo <= 'F'));
            i += 3;
        }

        auto const buf = percentDecode(enc_str);
        CHECK(str.size() == buf.size());
        CHECK(0 == std::memcmp(str.data(), buf.data(), buf.size()));
    }

    {
        auto a = Uuid{};
        auto b = Uuid{};

        randomize(a);
        randomize(b);

        auto const a1 = std::move(a);
        auto const b1 = std::move(b);
        CHECK(a1 == a);
        CHECK(b1 == b);
        std::ranges::swap(a, b);
        CHECK(a1 == b);
        CHECK(b1 == a);
    }

    {
        auto a = Uuid{};
        auto b = Uuid{};

        randomize(a);
        randomize(b);

        auto const a1 = a;
        auto const b1 = b;
        CHECK(a1 == a);
        CHECK(b1 == b);
        std::ranges::swap(a, b);
        CHECK(a1 == b);
        CHECK(b1 == a);
    }

    LOOP_N (100)
    {
        auto a = Uuid{};
        auto b = Uuid{};

        randomize(a);
        randomize(b);

        CHECK(a != b);
        CHECK(a < b || a > b);

        auto coll = UnorderedSet<Uuid>{};
        insert(coll, a);
        insert(coll, b);
        CHECK(2 == coll.size());
    }

    LOOP_N (100)
    {
        auto buf = genRandBuffer(1, 10000);
        CHECK(buf.size() > 0);

        auto const d1  = md5(buf);
        auto const shr = shell("md5sum 2>&1 | cut -d ' ' -f 1"_sv, buf).get();
        CHECK(0 == shr.status);
        CHECK(shr.cout.size() == 1);

        auto const d2 = hex2bin(shr.cout.front());
        CHECK(d2.size() == 16);
        CHECK(Md5(d2.data()) == d1);
    }

    LOOP_N (100)
    {
        auto fp_tmp = "/tmp"_fp / genUuidString();
        ON_SCOPE_EXIT(fs::remove(fp_tmp));

        auto key       = genRandBuffer(16, 128);
        auto s_hex_key = bin2hex(key);
        auto buf       = genRandBuffer(1, 10000);
        CHECK(buf.size() > 0);
        writeTo(fp_tmp, buf).get();

        auto const md5_sum    = md5(buf);
        auto const sha1_sum   = sha1(buf);
        auto const sha224_sum = sha224(buf);
        auto const sha256_sum = sha256(buf);
        auto const sha384_sum = sha384(buf);
        auto const sha512_sum = sha512(buf);

        {
            auto const shr = shell(scat("md5sum "sv,
                                        fp_tmp.native(),
                                        " 2>&1 | cut -d ' ' -f 1"sv))
                                 .get();
            CHECK(0 == shr.status);
            CHECK(shr.cout.size() == 1);

            auto const d2 = hex2bin(shr.cout.front());
            CHECK(d2.size() == 16);
            CHECK(Md5(d2.data()) == md5_sum);
        }

        {
            auto const shr = shell(scat("sha1sum "sv,
                                        fp_tmp.native(),
                                        " 2>&1 | cut -d ' ' -f 1"sv))
                                 .get();
            CHECK(0 == shr.status);
            CHECK(shr.cout.size() == 1);

            auto const d2 = hex2bin(shr.cout.front());
            CHECK(d2.size() == 20);
            CHECK(Sha1(d2.data()) == sha1_sum);
        }

        {
            auto const shr = shell(scat("sha224sum "sv,
                                        fp_tmp.native(),
                                        " 2>&1 | cut -d ' ' -f 1"sv))
                                 .get();
            CHECK(0 == shr.status);
            CHECK(shr.cout.size() == 1);

            auto const d2 = hex2bin(shr.cout.front());
            CHECK(d2.size() == 28);
            CHECK(Sha224(d2.data()) == sha224_sum);
        }

        {
            auto const shr = shell(scat("sha256sum "sv,
                                        fp_tmp.native(),
                                        " 2>&1 | cut -d ' ' -f 1"sv))
                                 .get();
            CHECK(0 == shr.status);
            CHECK(shr.cout.size() == 1);

            auto const d2 = hex2bin(shr.cout.front());
            CHECK(d2.size() == 32);
            CHECK(Sha256(d2.data()) == sha256_sum);
        }

        {
            auto const shr = shell(scat("sha384sum "sv,
                                        fp_tmp.native(),
                                        " 2>&1 | cut -d ' ' -f 1"sv))
                                 .get();
            CHECK(0 == shr.status);
            CHECK(shr.cout.size() == 1);

            auto const d2 = hex2bin(shr.cout.front());
            CHECK(d2.size() == 48);
            CHECK(Sha384(d2.data()) == sha384_sum);
        }

        {
            auto const shr = shell(scat("sha512sum "sv,
                                        fp_tmp.native(),
                                        " 2>&1 | cut -d ' ' -f 1"sv))
                                 .get();
            CHECK(0 == shr.status);
            CHECK(shr.cout.size() == 1);

            auto const d2 = hex2bin(shr.cout.front());
            CHECK(d2.size() == 64);
            CHECK(Sha512(d2.data()) == sha512_sum);
        }

        auto const hmac_md5    = hmacMd5(key, buf);
        auto const hmac_sha1   = hmacSha1(key, buf);
        auto const hmac_sha224 = hmacSha224(key, buf);
        auto const hmac_sha256 = hmacSha256(key, buf);
        auto const hmac_sha384 = hmacSha384(key, buf);
        auto const hmac_sha512 = hmacSha512(key, buf);

        {
            auto const shr =
                shell(scat("openssl dgst -md5 -mac HMAC -macopt hexkey:"sv,
                           s_hex_key,
                           " "sv,
                           fp_tmp.native(),
                           " 2>&1 | cut -d ' ' -f 2"sv))
                    .get();
            CHECK(0 == shr.status);
            CHECK(shr.cout.size() == 1);

            auto const d2 = hex2bin(shr.cout.front());
            CHECK(d2.size() == 16);
            CHECK(Md5(d2.data()) == hmac_md5);
        }

        {
            auto const shr =
                shell(scat("openssl dgst -sha1 -mac HMAC -macopt hexkey:"sv,
                           s_hex_key,
                           " "sv,
                           fp_tmp.native(),
                           " 2>&1 | cut -d ' ' -f 2"sv))
                    .get();
            CHECK(0 == shr.status);
            CHECK(shr.cout.size() == 1);

            auto const d2 = hex2bin(shr.cout.front());
            CHECK(d2.size() == 20);
            CHECK(Sha1(d2.data()) == hmac_sha1);
        }

        {
            auto const shr =
                shell(scat("openssl dgst -sha224 -mac HMAC -macopt hexkey:"sv,
                           s_hex_key,
                           " "sv,
                           fp_tmp.native(),
                           " 2>&1 | cut -d ' ' -f 2"sv))
                    .get();
            CHECK(0 == shr.status);
            CHECK(shr.cout.size() == 1);

            auto const d2 = hex2bin(shr.cout.front());
            CHECK(d2.size() == 28);
            CHECK(Sha224(d2.data()) == hmac_sha224);
        }

        {
            auto const shr =
                shell(scat("openssl dgst -sha256 -mac HMAC -macopt hexkey:"sv,
                           s_hex_key,
                           " "sv,
                           fp_tmp.native(),
                           " 2>&1 | cut -d ' ' -f 2"sv))
                    .get();
            CHECK(0 == shr.status);
            CHECK(shr.cout.size() == 1);

            auto const d2 = hex2bin(shr.cout.front());
            CHECK(d2.size() == 32);
            CHECK(Sha256(d2.data()) == hmac_sha256);
        }

        {
            auto const shr =
                shell(scat("openssl dgst -sha384 -mac HMAC -macopt hexkey:"sv,
                           s_hex_key,
                           " "sv,
                           fp_tmp.native(),
                           " 2>&1 | cut -d ' ' -f 2"sv))
                    .get();
            CHECK(0 == shr.status);
            CHECK(shr.cout.size() == 1);

            auto const d2 = hex2bin(shr.cout.front());
            CHECK(d2.size() == 48);
            CHECK(Sha384(d2.data()) == hmac_sha384);
        }

        {
            auto const shr =
                shell(scat("openssl dgst -sha512 -mac HMAC -macopt hexkey:"sv,
                           s_hex_key,
                           " "sv,
                           fp_tmp.native(),
                           " 2>&1 | cut -d ' ' -f 2"sv))
                    .get();
            CHECK(0 == shr.status);
            CHECK(shr.cout.size() == 1);

            auto const d2 = hex2bin(shr.cout.front());
            CHECK(d2.size() == 64);
            CHECK(Sha512(d2.data()) == hmac_sha512);
        }
    }

    LOOP_N (100)
    {
        auto const plaintext = genRandBuffer(1, 10000);
        auto const aad       = genUuidString();
        auto       key128    = AesKey128{};
        auto       key192    = AesKey192{};
        auto       key256    = AesKey256{};

        randomize(key128);
        randomize(key192);
        randomize(key256);

        auto opt_agm128_encoded = encrypt(key128, plaintext, aad);
        auto opt_agm192_encoded = encrypt(key192, plaintext, aad);
        auto opt_agm256_encoded = encrypt(key256, plaintext, aad);

        CHECK(opt_agm128_encoded->data.size() == plaintext.size());
        CHECK(opt_agm192_encoded->data.size() == plaintext.size());
        CHECK(opt_agm256_encoded->data.size() == plaintext.size());

        CHECK(opt_agm128_encoded->data != plaintext);
        CHECK(opt_agm192_encoded->data != plaintext);
        CHECK(opt_agm256_encoded->data != plaintext);

        CHECK(opt_agm128_encoded->aad == aad);
        CHECK(opt_agm192_encoded->aad == aad);
        CHECK(opt_agm256_encoded->aad == aad);

        auto opt_agm128_decoded = decrypt(key128, *opt_agm128_encoded);
        auto opt_agm192_decoded = decrypt(key192, *opt_agm192_encoded);
        auto opt_agm256_decoded = decrypt(key256, *opt_agm256_encoded);

        CHECK(opt_agm128_decoded->data == plaintext);
        CHECK(opt_agm192_decoded->data == plaintext);
        CHECK(opt_agm256_decoded->data == plaintext);

        CHECK(opt_agm128_decoded->aad == aad);
        CHECK(opt_agm192_decoded->aad == aad);
        CHECK(opt_agm256_decoded->aad == aad);

        CHECK(opt_agm128_decoded->iv == opt_agm128_encoded->iv);
        CHECK(opt_agm192_decoded->iv == opt_agm192_encoded->iv);
        CHECK(opt_agm256_decoded->iv == opt_agm256_encoded->iv);

        CHECK(opt_agm128_decoded->tag == opt_agm128_encoded->tag);
        CHECK(opt_agm192_decoded->tag == opt_agm192_encoded->tag);
        CHECK(opt_agm256_decoded->tag == opt_agm256_encoded->tag);
    }
}